<?php

namespace Fluency\Web;

use Fluency\Web\Http\ActiveRequest;
use Fluency\Web\Http\Response;
use Fluency\Utils\HeadTailList;
use Fluency\Utils\TransformingIterator;

/**
 * An action filter which renders the action result. This filter is appended to end of the filter
 * chain automatically by the RequestDispatcher. It can also be used to make renderer functions
 * separately from the filtering functionality, using the makeRenderer() method.
 *
 * @see Fluency\Web\FilterChain
 *
 * @package Fluency\Web
 * @author  Ignas Rudaitis
 * @since   1.0
 */
class RenderingFilter
{
    const VIEW_COMPOSITION_OPERATOR = '+';
    const ALTERNATE_VIEW_OPERATOR = '|';

    private $viewResolver;

    /**
     * The constructor. It takes the view resolver (a function that takes a name together with an
     * ActiveRequest and returns the view corresponding the name) as the only argument.
     *
     * @see Fluency\Web\RequestDispatcherBuilder::setViewResolver()
     *
     * @throws BadFunctionCallException if the view resolver is not callable
     * @param callable $viewResolver
     */
    public function __construct($viewResolver)
    {
        if (!is_callable($viewResolver)) {
            throw new \BadFunctionCallException("A view resolver has to be callable");
        }
        $this->viewResolver = $viewResolver;
    }

    /**
     * Returns a renderer function corresponding to the given identifier. The function takes a
     * single argument as the input and returns the rendering output.
     *
     * The plus and the pipe (vertical bar) characters are treated as operators (explained below)
     * in the identifier.
     *
     * Examples of identifiers:
     *
     * <ul>
     *   <li><code>"view"</code> renders the input using the view named "view"</li>
     *   <li><code>"layout + view"</code> renders it using the composition of views "layout" and
     *       "view"</li>
     *   <li><code>"layout + frame + #seq|empty"</code> uses the composition of views "layout",
     *       "frame" and "#seq", and if the input is empty, the view "#seq" is replaced with the
     *       view "empty"</li>
     *   <li><code>array("layout + #seq", "item")</code> accepts a Traversable or an array as the
     *       input, renders each element of it using the view "item" and also the whole collection
     *       using the composition of views "layout" and "#seq" (this is done by replacing the
     *       input collection with a TransformingIterator, which renders the elements when they
     *       are requested)</li>
     *   <li><code>array("layout + list|empty-list", "group|empty-group", "item")</code> accepts a
     *       two-dimensional Traversable/array input and renders the collection using the renderer
     *       "layout + list|empty-list", with all its elements of the first dimension rendered
     *       using "group|empty-group" and all the elements of the second dimension rendered using
     *       the view "item"</li>
     *   <li><code>null</code> or any other identifier that is recognized as empty by PHP
     *       corresponds to a renderer that simply returns the input coerced to a string</li>
     * </ul>
     *
     * The composition of views is exactly the same as function composition (e.g. (f + g)(x) =
     * f(g(x)) if the views are understood as functions.
     *
     * The special view name <code>#seq</code> is always resolved to a view that concatenates all
     * the items in the input collection and returns the result.
     *
     * There is also a special view named <code>#dump</code>, which returns the output of
     * var_dump() applied to the input.
     *
     * @param mixed $id the renderer identifier
     * @param Fluency\Web\Http\ActiveRequest $req the request to pass to the view resolver
     * @return callable
     */
    public function makeRenderer($id, ActiveRequest $req)
    {
        if (count($id) > 1) {
            $chain = array();
            foreach ((array) $id as $dimension) {
                $chain[] = $this->makeFlatRenderer($dimension, $req);
            }
            return function($x) use ($chain) {
                $first = array_shift($chain);
                return $first(new TransformingIterator($x, new HeadTailList($chain)));
            };
        } else {
            return $this->makeFlatRenderer($id, $req);
        }
    }

    private function makeFlatRenderer($id, ActiveRequest $req)
    {
        $names = array_map("trim", explode(self::VIEW_COMPOSITION_OPERATOR, $id));
        $views = array();
        foreach ($names as $name) {
            $views[] = $this->makeAtomicRenderer($name, $req);
        }
        return $this->composeFunctions($views);
    }

    private function makeAtomicRenderer($id, ActiveRequest $req)
    {
        $views = array_map("trim", explode(self::ALTERNATE_VIEW_OPERATOR, $id));
        $primaryView = $this->resolveView(reset($views), $req);
        if (count($views) > 1) {
            $alternateView = $this->resolveView(end($views), $req);
            return function($x) use ($primaryView, $alternateView) {
                return (!empty($x) && count($x) != 0) ? $primaryView($x) : $alternateView($x);
            };
        } else {
            return $primaryView;
        }
    }

    private function resolveView($name, $req)
    {
        if (empty($name)) {
            $view = function($x) { echo $x; };
        } elseif ($name === "#seq") {
            $view = function($xs) { foreach ($xs as $x) { echo $x; } };
        } elseif ($name === "#dump") {
            $view = function($x) { echo "<pre>\n"; var_dump($x); echo "</pre>\n"; };
        } else {
            $viewResolver = $this->viewResolver;
            $view = $viewResolver($name, $req);
        }
        return $this->wrapViewInOutputCapturer($view);
    }

    private function wrapViewInOutputCapturer($view)
    {
        return function($x) use ($view) {
            ob_start();
            $result = $view($x);
            $ob = ob_get_contents();
            ob_end_clean();
            return $ob . $result;
        };
    }

    private function composeFunctions(array $functions)
    {
        $functions = array_reverse($functions);
        return function($x) use ($functions) {
            foreach ($functions as $f) {
                $x = $f($x);
            }
            return $x;
        };
    }

    /**
     * Serves as an action filter by returning a filter continuation which takes the action result
     * and returns a Response object. The response consists of the rendered result as the response
     * body and of HTTP headers fetched from Header fields of the RespondingPolicy. The renderer
     * identifier is also taken from the RespondingPolicy, from the last Renderer field (if no such
     * field exists, NULL is used).
     *
     * @see makeRenderer()
     * @see Fluency\Web\FilterChain
     * @see Fluency\Web\RespondingPolicy
     *
     * @param Fluency\Web\Http\ActiveRequest $req the request to fetch the RespondingPolicy from
     * @return callable
     */
    public function __invoke(ActiveRequest $req)
    {
        $self = $this;
        return function($result) use ($req, $self) {
            $rendererField = ucfirst(strtolower($req->getFormat())) . "Renderer";
            $policy = $req->getRespondingPolicy();
            $rendererId = $policy->getLastFieldOfType($rendererField, $policy->getRenderer(null));
            $renderer = $self->makeRenderer($rendererId, $req);
            $body = $renderer($result);
            if ($body instanceof Response) {
                return $body;
            }
            $headers = $req->getRespondingPolicy()->getAllFieldsOfType("Header");
            return new Response($body, $headers);
        };
    }
}
